// Copyright (c) 2022. China Mobile(SuZhou)Software Technology Co.,Ltd. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package backend

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/parnurzeal/gorequest"

	"gitee.com/he3db/he3proxy/config"
	"gitee.com/he3db/he3proxy/core/golog"
)

const (
	CPU_WEIGHT        = 1
	MEM_WEIGHT        = 3
	DISK_IO_WEIGHT    = 3
	CONNECTION_WEIGHT = 3
)

type Prometheus struct {
	Status string `json:"status"`
	Data   Data   `json:"data"`
}

type Data struct {
	ResultType string   `json:"resultType"`
	Result     []Result `json:"result"`
}

type Result struct {
	Metric Metric        `json:"metric"`
	Values []interface{} `json:"values"`
	Value  []interface{} `json:"value"`
}

type Metric struct {
	Device   string `json:"device"`
	Instance string `json:"instance"`
	Job      string `json:"job"`
}

func CalculateAndChooseBestNode(cfg *config.NodeConfig, nodeLoad map[int]int) (int, map[int]int, error) {
	index := 0
	maxScore := 0
	pgPrometheusUrl := cfg.PgPrometheusAddr

	nodeExporterSlice := make([]string, 0)
	pgExporterSlice := make([]string, 0)
	for _, k := range cfg.MonitorPgNodes {
		tmp := strings.Split(k, "@")
		if len(tmp) != 3 {
			return -1, nil, errors.New(fmt.Sprintf("parse config file error, parameter monitor_pg_node: %v\n", k))
		}
		pgExporterSlice = append(pgExporterSlice, tmp[0]+":"+tmp[1])
		nodeExporterSlice = append(nodeExporterSlice, tmp[0]+":"+tmp[2])
	}

	for i := 0; i < len(cfg.MonitorPgNodes); i++ {
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("handle info with node: %s \n", cfg.MonitorPgNodes[i]), 0)
		connNum, err := getCurrentConnections(pgPrometheusUrl, pgExporterSlice[i], cfg.PgExporterName)
		if err != nil {
			golog.Error("pg prometheus metric", "CalculateAndChooseBestNode",
				fmt.Sprintf("Get current connections failed: %s \n", err.Error()), 0)
			return -1, nil, errors.New(fmt.Sprintf("Get current connections failed: %s \n", err.Error()))
		}
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("current connection num: %d \n", connNum), 0)

		cpuIdle, err := getCpuIdle(pgPrometheusUrl, nodeExporterSlice[i], cfg.NodeExporterName, cfg.NodeCpuMode, cfg.TimeInterval)
		if err != nil {
			golog.Error("pg prometheus metric", "CalculateAndChooseBestNode",
				fmt.Sprintf("Get current cpu idle failed: %s \n", err.Error()), 0)
			return -1, nil, errors.New(fmt.Sprintf("Get current cpu idle failed: %s \n", err.Error()))
		}
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("current cpu idle: %d \n", cpuIdle), 0)

		memIdle, err := getMemIdle(pgPrometheusUrl, nodeExporterSlice[i], cfg.NodeExporterName)
		if err != nil {
			golog.Error("pg prometheus metric", "CalculateAndChooseBestNode",
				fmt.Sprintf("Get current mem idle failed: %s \n", err.Error()), 0)
			return -1, nil, errors.New(fmt.Sprintf("Get current mem idle failed: %s \n", err.Error()))
		}
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("current mem idle: %d \n", memIdle), 0)

		ioUtilization, err := getDiskIOUtilization(pgPrometheusUrl, nodeExporterSlice[i], cfg.NodeExporterName, cfg.PgDataDiskName, cfg.TimeInterval)
		if err != nil {
			golog.Error("pg prometheus metric", "CalculateAndChooseBestNode",
				fmt.Sprintf("Get current disk io utilization failed: %s \n", err.Error()), 0)
			return -1, nil, errors.New(fmt.Sprintf("Get current disk io utilization failed: %s \n", err.Error()))
		}
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("current disk io utilization: %d \n", ioUtilization), 0)
		score := cpuIdle*CPU_WEIGHT + memIdle*MEM_WEIGHT + ioUtilization*DISK_IO_WEIGHT + (cfg.MaxConnNum-connNum)*CONNECTION_WEIGHT
		nodeLoad[i] = score
		if score > maxScore {
			index = i
			maxScore = score
		}
		golog.Debug("pg prometheus metric", "CalculateAndChooseBestNode",
			fmt.Sprintf("current score is %d, max score is %d \n", score, maxScore), 0)
	}
	return index, nodeLoad, nil
}

func getCurrentConnections(prometheusUrl string, pgInstanceLable string, pgJobLable string) (int, error) {
	var metric Prometheus
	request := gorequest.New()
	resp, _, errs := request.Get(prometheusUrl).
		Param("query", "sum(pg_stat_database_numbackends{instance=\""+pgInstanceLable+"\",job=\""+pgJobLable+"\"})").
		EndStruct(&metric)
	if errs != nil || resp.StatusCode > 300 || resp.StatusCode < 200 {
		golog.Error("pg prometheus metric", "getCurrentConnections",
			fmt.Sprintf("%v %v", resp, errs), 0)
		return 0, errors.New("Failed to get current connections. ")
	} else {
		return handleResult(metric)
	}
}

func getCpuIdle(prometheusUrl string, nodeInstanceLable string, nodeJobLable string, mode string, timeInterval string) (int, error) {
	var metric Prometheus
	request := gorequest.New()
	resp, _, errs := request.Get(prometheusUrl).
		Param("query", "sum(increase(node_cpu_seconds_total{instance=\""+nodeInstanceLable+
			"\",job=\""+nodeJobLable+"\",mode=\""+mode+"\"}["+timeInterval+
			"]))/sum(increase(node_cpu_seconds_total{instance=\""+nodeInstanceLable+
			"\",job=\""+nodeJobLable+"\"}["+timeInterval+"]))*100").
		EndStruct(&metric)
	if errs != nil || resp.StatusCode > 300 || resp.StatusCode < 200 {
		golog.Error("pg prometheus metric", "getCpuIdle",
			fmt.Sprintf("%v %v", resp, errs), 0)
		return 0, errors.New("Failed to get cpu idle. ")
	} else {
		return handleResult(metric)
	}
}

func getMemIdle(prometheusUrl string, nodeInstanceLable string, nodeJobLable string) (int, error) {
	var metric Prometheus
	request := gorequest.New()
	resp, _, errs := request.Get(prometheusUrl).
		Param("query", "node_memory_MemFree_bytes{instance=\""+nodeInstanceLable+
			"\",job=\""+nodeJobLable+"\"}/node_memory_MemTotal_bytes{instance=\""+nodeInstanceLable+"\",job=\""+nodeJobLable+"\"}*100").
		EndStruct(&metric)
	if errs != nil || resp.StatusCode > 300 || resp.StatusCode < 200 {
		golog.Error("pg prometheus metric", "getMemIdle",
			fmt.Sprintf("%v %v", resp, errs), 0)
		return 0, errors.New("Failed to get mem idle. ")
	} else {
		return handleResult(metric)
	}
}

func getDiskIOUtilization(prometheusUrl string, nodeInstanceLable string, nodeJobLable string, diskDevice string, timeInterval string) (int, error) {
	var metric Prometheus
	request := gorequest.New()
	resp, _, errs := request.Get(prometheusUrl).
		Param("query", "rate(node_disk_io_time_seconds_total{instance=\""+nodeInstanceLable+
			"\",job=\""+nodeJobLable+"\",device=~\""+diskDevice+"\"}["+timeInterval+"])*100").
		EndStruct(&metric)
	if errs != nil || resp.StatusCode > 300 || resp.StatusCode < 200 {
		golog.Error("pg prometheus metric", "getDiskIOUtilization",
			fmt.Sprintf("%v %v", resp, errs), 0)
		return 0, errors.New("Failed to get disk io utilization idle. ")
	} else {
		return handleResult(metric)
	}
}

func handleResult(metric Prometheus) (int, error) {
	defer func() {
		err := recover()
		if err != nil {
			golog.Error("pg prometheus metric", "handleResult",
				fmt.Sprintf("%v", err), 0)
		}
	}()
	t1 := metric.Data.Result[0].Value[1]
	switch t1.(type) {
	case uint:
		return int(t1.(uint)), nil
	case uint8:
		return int(t1.(uint8)), nil
	case uint16:
		return int(t1.(uint16)), nil
	case int8:
		return int(t1.(int8)), nil
	case int16:
		return int(t1.(int16)), nil
	case int32:
		return int(t1.(int32)), nil
	case uint32:
		return int(t1.(uint32)), nil
	case int64:
		return int(t1.(int64)), nil
	case uint64:
		return int(t1.(uint64)), nil
	case float32:
		return int(t1.(float32)), nil
	case float64:
		return int(t1.(float64)), nil
	case string:
		f, err := strconv.ParseFloat(t1.(string), 64)
		return int(f), err
	default:
		return t1.(int), nil
	}
}
